package jmine.tec.rpc.server.representation;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import jmine.tec.rpc.common.entity.AbstractEntityRepresentation;
import jmine.tec.rpc.common.entity.EntityRepresentation;
import jmine.tec.rpc.server.base.RestServiceErrorMessages;
import jmine.tec.rpc.server.base.RpcServerException;
import jmine.tec.rpc.server.entity.EntityRestMapping;

import org.apache.commons.beanutils.PropertyUtils;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BaseDAOFactory;
import bancosys.tec.persist.dao.BeanNotFoundException;

/**
 * Update an entity object (Persistible implementor) from a representation (EntityRepresentation) object.
 * 
 * @author fernando.silva
 */
public class EntityUpdaterFromRepresentation {

    private BaseDAOFactory baseDAOFactory;

    private EntityRestMapping entityRestMapping;

    /**
     * Set the BaseDAOFactory for the service.
     * 
     * @param baseDAOFactory The factory for persistable DAO beans.
     */
    public void setBaseDAOFactory(BaseDAOFactory baseDAOFactory) {
        this.baseDAOFactory = baseDAOFactory;
    }

    /**
     * Inject the EntityRestMapping.
     * 
     * @param entityRestMapping Objeto da classe
     */
    public void setEntityRestMapping(EntityRestMapping entityRestMapping) {
        this.entityRestMapping = entityRestMapping;
    }

    /**
     * Copy field values from one bean to another.
     * 
     * @param <E> A Persistable object.
     * @param <RS> An EntityRepresentation object.
     * @param entity Target object.
     * @param representation Source object.
     */
    public <E extends Persistable, RS extends EntityRepresentation> void updateEntityFromRepresentation(E entity, RS representation) {

        // Get properties from entity bean using PropertyUtils (from org.apache.commons.beanutils)
        Map<String, PropertyDescriptor> entityDescriptors = new HashMap<String, PropertyDescriptor>();
        for (PropertyDescriptor aProperty : PropertyUtils.getPropertyDescriptors(entity)) {
            if (!aProperty.getName().equals("class")) {
                entityDescriptors.put(aProperty.getName(), aProperty);
            }
        }

        // Get properties from representation bean using PropertyUtils (from org.apache.commons.beanutils)
        Map<String, PropertyDescriptor> representationDescriptors = new HashMap<String, PropertyDescriptor>();
        for (PropertyDescriptor aProperty : PropertyUtils.getPropertyDescriptors(representation)) {
            if (!aProperty.getName().equals("class")) {
                representationDescriptors.put(aProperty.getName(), aProperty);
            }
        }

        // Copy properties from source to target since each one has the same property name and the source has a getter and the target has a
        // setter.
        PropertyDescriptor sourceProperty, targetProperty;
        for (String sourceKey : representationDescriptors.keySet()) {
            for (String targetKey : entityDescriptors.keySet()) {
                if ((sourceKey.equals(targetKey)) && (representationDescriptors.get(sourceKey).getReadMethod() != null)
                        && (entityDescriptors.get(targetKey).getWriteMethod() != null)) {

                    sourceProperty = representationDescriptors.get(sourceKey);
                    targetProperty = entityDescriptors.get(targetKey);

                    Object sourceObject;

                    try {
                        sourceObject = sourceProperty.getReadMethod().invoke(representation);
                    } catch (IllegalArgumentException e) {
                        throw new RpcServerException(RestServiceErrorMessages.CANNOT_READ_FROM_REPRESENTATION_OBJECT_ERROR.create(), e);
                    } catch (IllegalAccessException e) {
                        throw new RpcServerException(RestServiceErrorMessages.CANNOT_READ_FROM_REPRESENTATION_OBJECT_ERROR.create(), e);
                    } catch (InvocationTargetException e) {
                        throw new RpcServerException(RestServiceErrorMessages.CANNOT_READ_FROM_REPRESENTATION_OBJECT_ERROR.create(), e);
                    }

                    Class<?> targetClass = targetProperty.getPropertyType();
                    Object targetObject = this.getEntityFieldObjectGivenRepresentationFieldObject(sourceObject, targetClass);

                    try {
                        targetProperty.getWriteMethod().invoke(entity, targetObject);
                    } catch (IllegalArgumentException e) {
                        throw new RpcServerException(RestServiceErrorMessages.CANNOT_WRITE_TO_ENTITY_OBJECT_ERROR.create(), e);
                    } catch (IllegalAccessException e) {
                        throw new RpcServerException(RestServiceErrorMessages.CANNOT_WRITE_TO_ENTITY_OBJECT_ERROR.create(), e);
                    } catch (InvocationTargetException e) {
                        throw new RpcServerException(RestServiceErrorMessages.CANNOT_WRITE_TO_ENTITY_OBJECT_ERROR.create(), e);
                    }

                }
            }
        }

    }

    /**
     * Get the value to be stored on a entity object based on the original value and the target class.
     * 
     * @param <T> A class of the target class.
     * @param originalValue The original value on the representation object.
     * @param targetClass The expected returning class.
     * @return The value to be stored on the entity object.
     */
    @SuppressWarnings("unchecked")
    private <T extends Object> Object getEntityFieldObjectGivenRepresentationFieldObject(Object originalValue, Class<T> targetClass) {

        // Caso estúpido. Pode ocorrer.
        if (originalValue == null) {
            return null;
        }

        // Trato casos de collections
        if (Collection.class.isAssignableFrom(originalValue.getClass()) && Collection.class.isAssignableFrom(targetClass)) {
            // Crio uma coleção de objetos de mesmo tipo e chamo recursivo.
            Collection<Object> fromCollection = (Collection<Object>) originalValue;
            Collection<T> toCollection;
            try {
                toCollection = (Collection<T>) targetClass.newInstance();
            } catch (InstantiationException e) {
                throw new RpcServerException(RestServiceErrorMessages.CANNOT_WRITE_TO_ENTITY_OBJECT_ERROR.create(), e);
            } catch (IllegalAccessException e) {
                throw new RpcServerException(RestServiceErrorMessages.CANNOT_WRITE_TO_ENTITY_OBJECT_ERROR.create(), e);
            }

            for (Object eachObject : fromCollection) {
                toCollection.add((T) this.getEntityFieldObjectGivenRepresentationFieldObject(eachObject, targetClass.getComponentType()));
            }

            return toCollection;
        }

        // Trato casos de entidade
        if (AbstractEntityRepresentation.class.isAssignableFrom(originalValue.getClass())) {
            Class<EntityRepresentation> entityClass = (Class<EntityRepresentation>) originalValue.getClass();
            BaseDAO<Persistable> dao = this.baseDAOFactory.getGenericDAO(this.entityRestMapping.getEntityFor(entityClass));
            Persistable newEntity;
            try {
                Serializable pk = ((AbstractEntityRepresentation) originalValue).getPk();
                if (pk == null) {
                    throw new RpcServerException(RestServiceErrorMessages.REPRESENTATION_OBJECT_DOES_NOT_HAVE_PRIMARY_KEY.create());
                }
                newEntity = dao.findByPk(pk);
            } catch (BeanNotFoundException e) {
                throw new RpcServerException(RestServiceErrorMessages.REPRESENTED_ENTITY_NOT_FOUND_ON_DATABASE_ERROR.create(), e);
            }
            return newEntity;
        }

        return originalValue;
    }
}
